# G6.js 性能优化
# behavior
# drag-canvas
拖动会频繁触发updateViewport
// 去除无用逻辑 + 使用定时器,类nextTick原理去减少translate操作
// 实测setTimeout比requestAnimationFrame流畅一点
updateViewport(e) {
if (!this.queue) {
this.queue = [];
}
this.queue.push(() => {
const origin = this.origin;
const clientX = +e.clientX;
const clientY = +e.clientY;
if (isNaN(clientX) || isNaN(clientY) || !origin) {
return;
}
const dx = clientX - origin.x;
const dy = clientY - origin.y;
this.origin = {
x: clientX,
y: clientY
};
this.graph.translate(dx, dy);
});
if (!this.doing) {
this.doing = true;
this.timer = setTimeout(() => {
this.doing = false;
this.queue.pop()();
this.queue = [];
clearTimeout(this.timer);
this.timer = null;
});
}
}
# zoom
项目需要在缩放时改变显示的图片,
首先做了200ms的防抖
首次获取节点后缓存到graph实例上,避免每次都去取。在更新数据时要记得清除缓存
data.nodes = graph.getNodes();
data.edges = graph.getEdges();
data.combos = graph.getCombos();
图片根据缩放有几个档次,档次没变化时不对元素派发缩放事件
分批派发事件,截取指定数量的元素异步一批一批的派发事件。
# shape
# nodes
# 改变keyShape
开启优化enableOptimize后,拖动时只会显示keyshape。
但我们存在不同缩放比例时显示不同形状的需求,这里就需要动态的改变keyshape。
keyShape的实例上有个变量isKeyShape
记录,在需要的时机改变对应shape的isKeyShape
即可。
# 边颜色问题
因为画布是整个进行缩放的,所以在很小的时候,1px的边显示上是小于1px的,导致视觉上颜色变浅。
# 解决方案
在缩放的回调中,动态改变边的宽度。
当缩放比例小于1时,将需要显示的宽度除以当前的缩放比例,即将宽度放大了。
这里要给个最大值,不然有些本身就粗的线会变的很粗。
const needWidth = group.lineStatusConfig.statusInfo[configKey].width;
let showWidth = needWidth;
if (this.zoom && this.zoom < 1) {
showWidth = needWidth / this.zoom;
if (showWidth > 4) {
showWidth = 4;
}
}
# 节点位置整理
# 拖动自动吸附
G6提供了snapline 辅助线插件,但是居然不会自动吸附,所以需要自己拓展一下
# 原理
在addAlignLine
画完辅助线之后,根据辅助线的配置,找到最近的两条线的偏移量,改变拖动中的元素的位置即可,这里做的比较简单。
# 源码
/**
* 自动吸附
*/
autoAdsorbFn(alignCfg) {
const offset = {};
const graph = this.get('graph');
if (alignCfg) {
const { verticals, horizontals } = alignCfg;
const model = this.dragItem.getModel();
const updateModel = {};
if (verticals && verticals[0]) {
offset.x = verticals[0].dis;
updateModel.x = model.x + offset.x;
}
if (horizontals && horizontals[0]) {
offset.y = horizontals[0].dis;
updateModel.y = model.y + offset.y;
}
if (Object.keys(updateModel).length) {
graph.updateItem(
this.dragItem,
updateModel
);
}
}
// 记录偏移量,在drag-end后更新位置的地方要加上
graph.set('autoAdsorbOffset', offset);
}
需要注意的是,在拖动结束后,会发现位置的偏移。这是因为吸附是我们手动移的位置,dragend那获取到的坐标是鼠标的坐标。所以在做吸附偏移后,需记录上偏移量,在dragend的改变节点位置的操作中加上偏移量即可。
# 对齐功能
G6没提供选中几个节点去做对齐的功能,这里自己拓展一下。
主要做了一下几种对齐方式:
- 左对齐
- 右对齐
- 水平居中对齐
- 顶部对齐
- 底部对齐
- 垂直居中对齐
- 水平等间距对齐
- 垂直等间距对齐
# 上下左右对齐
直接找出最小的值,全部给那个值即可
# 水平、垂直中心对齐
找出两侧的值,求中心点即可
# 水平、垂直 等间距排序
间距 = (总长度 - 节点总长度) / (节点数 - 1)
再从小的开始,改变每一个的坐标
坐标 += 间距 + 前一个节点的长度
ps:长度指宽或者高
# 源码
/**
* 节点对齐操作
*/
// 更新的数据
let updateModels = {};
// 旧数据
let originModels = {};
/**
* 获取节点数据
* @param selectItem
* @returns {*}
*/
function getNodes(selectItem) {
return selectItem.nodes.map(item => {
const model = item.getModel();
return {
node: item,
model: model,
x: model.x,
y: model.y
};
});
}
/**
* 获取极值
* @param type
* @param key x,y
* @param selectItem
*/
function findTarget(type, key, selectItem) {
let target = selectItem[0][key];
for (let i = 1; i < selectItem.length; i++) {
const item = selectItem[i][key];
switch (type) {
case 'l':
case 't':
if (item < target) {
target = item;
}
break;
case 'r':
case 'b':
if (item > target) {
target = item;
}
break;
}
}
return target;
}
/**
* 上下左右对齐
* @param graph
* @param type
* @param selectItem
* @param params
*/
function alignNormal (graph, type, selectItem, params) {
const { positionKey } = params;
const target = findTarget(type, positionKey, selectItem);
selectItem.forEach(item => {
updateItem(graph, item, positionKey, target);
});
}
/**
* 水平、垂直中心对齐
* @param graph
* @param type
* @param selectItem
* @param params
*/
function alignCenter(graph, type, selectItem, params) {
// 找出x左右取中间值
const { positionKey, minType, maxType } = params;
const min = findTarget(minType, positionKey, selectItem);
const max = findTarget(maxType, positionKey, selectItem);
const mid = (min + max) / 2;
selectItem.forEach(item => {
updateItem(graph, item, positionKey, mid);
});
}
/**
* 水平、垂直 平均间隙排序
* @param graph
* @param type
* @param selectItem
* @param params
* @returns {{updateModels: {}, originModels: {}}}
*/
function alignGap(graph, type, selectItem, params) {
const { length } = selectItem;
// 小于3不需要动
if (length < 3) {
return;
}
const { positionKey, lengthKey } = params;
// 小的在前面
selectItem.sort((a, b) => {
return a[positionKey] - b[positionKey];
});
// 设备总长度
let totalItemLength = 0;
selectItem.forEach(item => {
item[lengthKey] = item.node.getCanvasBBox()[lengthKey];
totalItemLength += item[lengthKey];
});
const min = selectItem[0][positionKey];
const max = selectItem[length - 1][positionKey] + selectItem[length - 1][lengthKey];
// 选中的总长度
const totalLength = max - min;
// 节点间间距
const gap = (totalLength - totalItemLength) / (selectItem.length - 1);
// 每个节点要给的位置
let position = min;
for (let i = 1; i < length - 1; i++) {
const item = selectItem[i];
position += gap + selectItem[i - 1][lengthKey];
updateItem(graph, item, positionKey, position);
}
}
/**
* 更新节点属性
* @param graph
* @param item
* @param positionKey
* @param newValue
*/
function updateItem(graph, item, positionKey, newValue) {
const id = item.model.id;
updateModels[id] = {
[positionKey]: newValue
};
originModels[id] = {
[positionKey]: item[positionKey]
};
graph.updateItem(
item.node,
updateModels[id]
);
}
const ALIGN_TYPE = {
l: {
fn: alignNormal,
params: {
positionKey: 'x'
}
},
r: {
fn: alignNormal,
params: {
positionKey: 'x'
}
},
t: {
fn: alignNormal,
params: {
positionKey: 'y'
}
},
b: {
fn: alignNormal,
params: {
positionKey: 'y'
}
},
lrc: {
fn: alignCenter,
params: {
positionKey: 'x',
minType: 'l',
maxType: 'r'
}
},
tbc: {
fn: alignCenter,
params: {
positionKey: 'y',
minType: 't',
maxType: 'b'
}
},
horizon: {
fn: alignGap,
params: {
positionKey: 'x',
lengthKey: 'width'
}
},
vertical: {
fn: alignGap,
params: {
positionKey: 'y',
lengthKey: 'height'
}
}
};
export default function changeAlign(graph, type, selectItem) {
const match = ALIGN_TYPE[type];
// 重置记录
updateModels = {};
originModels = {};
match.fn(graph, type, getNodes(selectItem), match.params);
// 记录更改状态 有才改
if (Object.keys(updateModels).length) {
graph.execCmd('batchUpdate', {
originModels: originModels,
updateModels: updateModels
});
}
}
# 减少图元
图元的数量直接影响性能,所以需要尽可能的减少绘制的图元。
# 节点
- 排查发现,节点使用了多套尺寸的样式,与设计同步后决定去除小尺寸的样式。
- 有些显示项是可以控制显示的,所以不显示的改为动态绘制。
# 边
边同理,当前不显示的就不绘制,显示时才绘制。
# 优化内存
模拟5000+设备发现内存直接占用到了1.5G+,这是不能接受的。
排查发现每个节点都存储了其所有子节点的数据,相当于存了无数份树结构的数据在内存中。
改为节点只记录子节点的id,需要用时通过id去全局的唯一一份Map数据中取。
优化后1w+设备只占用了400M的内存。
# 杂项
# 文字超出宽度显示省略号
canvas中没法像css那样加几个属性就可以显示省略号了,所以这里得手动实现
# 思路
主要是英文算1px,中文算2px,用盒子宽度除以全英文得到最大显示字数,再循环文字,文字长度大于最大时就裁剪掉,拼上省略号。
# 实现
/**
* 超出宽度的文字添加省略号
* @param text
* @param width
* @param fontSize
* @returns {string|*}
*/
export function addLongTextEllipsis(text, width, fontSize) {
// 宽度除以字体大小,需乘以2才是全英文的最大长度,再减去3(省略号)减去2(左右加点间距)
const maxLen = Math.floor(width / fontSize) * 2 - 3 - 2;
if (text.length === 0 || maxLen <= 0) {
return text;
}
let len = 0;
let needPreCut = false;
let i = 0;
for (; i < text.length; i++) {
if (text.charCodeAt(i) > 255) {
len += 2;
} else {
len += 1;
}
if (len > maxLen) {
needPreCut = true;
break;
}
}
if (needPreCut) {
return text.substr(0, i) + '...';
} else {
return text;
}
}